Package org.python.pydev.editor.autoedit

Source Code of org.python.pydev.editor.autoedit.PyAutoIndentStrategy

/**
* Copyright (c) 2005-2011 by Appcelerator, Inc. All Rights Reserved.
* Licensed under the terms of the Eclipse Public License (EPL).
* Please see the license.txt included with this distribution for details.
* Any modifications to this file must keep this entire header intact.
*/
/*
* Created on Dec 10, 2003
* Author: atotic
*/

package org.python.pydev.editor.autoedit;

import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.Document;
import org.eclipse.jface.text.DocumentCommand;
import org.eclipse.jface.text.IAutoEditStrategy;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.TextSelection;
import org.python.pydev.core.IIndentPrefs;
import org.python.pydev.core.docutils.ImportsSelection;
import org.python.pydev.core.docutils.NoPeerAvailableException;
import org.python.pydev.core.docutils.ParsingUtils;
import org.python.pydev.core.docutils.PySelection;
import org.python.pydev.core.docutils.PySelection.LineStartingScope;
import org.python.pydev.core.docutils.PythonPairMatcher;
import org.python.pydev.core.docutils.StringUtils;
import org.python.pydev.core.docutils.SyntaxErrorException;
import org.python.pydev.core.log.Log;
import org.python.pydev.editor.actions.PyAction;
import org.python.pydev.plugin.PydevPlugin;

import com.aptana.interactive_console.console.ui.internal.IHandleScriptAutoEditStrategy;
import com.aptana.shared_core.string.FastStringBuffer;
import com.aptana.shared_core.structure.Tuple;
import com.aptana.shared_core.utils.DocCmd;

/**
* Class which implements the following behaviors:
* - indenting: after 'class' or 'def'
* - replacement: when typing colons or parentheses
*
* This class uses the org.python.pydev.core.docutils.DocUtils class extensively
* for some document-related operations.
*/
public final class PyAutoIndentStrategy implements IAutoEditStrategy, IHandleScriptAutoEditStrategy {

    private IIndentPrefs prefs;

    private boolean blockSelection;

    public PyAutoIndentStrategy() {
    }

    public void setIndentPrefs(IIndentPrefs prefs) {
        this.prefs = prefs;
    }

    public IIndentPrefs getIndentPrefs() {
        if (this.prefs == null) {
            if (PydevPlugin.getDefault() == null) {
                this.prefs = new TestIndentPrefs(true, 4);
            } else {
                this.prefs = new DefaultIndentPrefs(); //create a new one (because each pyedit may force the tabs differently).
            }
        }
        return this.prefs;
    }

    /**
     * Set indentation automatically after newline.
     *
     * @return tuple with the indentation to be set and a boolean determining if it was found
     * to be within a parenthesis or not.
     */
    private Tuple<String, Boolean> autoIndentNewline(IDocument document, int length, String text, int offset)
            throws BadLocationException {

        if (offset > 0) {
            PySelection selection = new PySelection(document, offset);

            String lineWithoutComments = selection.getLineContentsToCursor(true, true);

            Tuple<Integer, Boolean> tup = determineSmartIndent(offset, document, prefs);
            int smartIndent = tup.o1;
            boolean isInsidePar = tup.o2;

            if (lineWithoutComments.length() > 0) {
                //ok, now let's see the auto-indent
                int curr = lineWithoutComments.length() - 1;
                char lastChar = lineWithoutComments.charAt(curr);

                //we dont want whitespaces
                while (curr > 0 && Character.isWhitespace(lastChar)) {
                    curr--;
                    lastChar = lineWithoutComments.charAt(curr);
                }

                //we have to check if smartIndent is -1 because otherwise we are inside some bracket
                if (smartIndent == -1 && !isInsidePar && StringUtils.isClosingPeer(lastChar)) {
                    //ok, not inside brackets
                    PythonPairMatcher matcher = new PythonPairMatcher(StringUtils.BRACKETS);
                    int bracketOffset = selection.getLineOffset() + curr;
                    IRegion region = matcher.match(document, bracketOffset + 1);
                    if (region != null) {
                        if (!PySelection.endsInSameLine(document, region)) {
                            //we might not have a match if there is an error in the program...
                            //e.g. a single ')' without its counterpart.
                            int openingBracketLine = document.getLineOfOffset(region.getOffset());
                            String openingBracketLineStr = PySelection.getLine(document, openingBracketLine);
                            int first = PySelection.getFirstCharPosition(openingBracketLineStr);
                            String initial = getCharsBeforeNewLine(text);
                            text = initial + openingBracketLineStr.substring(0, first);
                            return new Tuple<String, Boolean>(text, isInsidePar);
                        }
                    }
                } else if (smartIndent == -1 && lastChar == ':') {
                    //we have to check if smartIndent is -1 because otherwise we are in a dict
                    //ok, not inside brackets
                    text = indentBasedOnStartingScope(text, selection, false);
                    return new Tuple<String, Boolean>(text, isInsidePar);
                }
            }

            String trimmedLine = lineWithoutComments.trim();

            if (smartIndent >= 0
                    && (StringUtils.hasOpeningBracket(trimmedLine) || StringUtils.hasClosingBracket(trimmedLine))) {
                return new Tuple<String, Boolean>(makeSmartIndent(text, smartIndent), isInsidePar);
            }
            //let's check for dedents...
            if (PySelection.startsWithDedentToken(trimmedLine)) {
                if (lineWithoutComments.endsWith("\\")) {
                    //Okay, we're in something as return \, where the next line will be part of this statement, so, don't really
                    //go back an indent, but go up an indent.
                    return new Tuple<String, Boolean>(text + prefs.getIndentationString(), isInsidePar);
                }
                return new Tuple<String, Boolean>(dedent(text), isInsidePar);
            }

            boolean indentBasedOnStartingScope = false;
            try {
                if (PySelection.containsOnlyWhitespaces(selection.getLineContentsFromCursor())) {
                    indentBasedOnStartingScope = true;
                }
            } catch (BadLocationException e) {
                //(end of the file)
                indentBasedOnStartingScope = true;
            }

            if (indentBasedOnStartingScope) {
                String lineContentsToCursor = selection.getLineContentsToCursor();
                String trimmed = lineContentsToCursor.trim();
                if (trimmed.length() == 0) {
                    return new Tuple<String, Boolean>(indentBasedOnStartingScope(text, selection, false), isInsidePar);
                } else {
                    boolean endsWithTrippleSingle = trimmed.endsWith("'''");
                    if (endsWithTrippleSingle || trimmed.endsWith("\"\"\"")) {
                        //ok, as we're out of a string scope at this point, this means we just closed a string, so,
                        //we should go back to indent based on starting scope.

                        if (endsWithTrippleSingle) {
                            int cursorLine = -1;
                            try {
                                ParsingUtils parsingUtils = ParsingUtils.create(selection.getDoc(), true);
                                int cursorOffset = selection.getAbsoluteCursorOffset();
                                char c;
                                do {
                                    cursorOffset--;
                                    c = parsingUtils.charAt(cursorOffset);

                                } while (Character.isWhitespace(c));

                                int startOffset = parsingUtils.eatLiteralsBackwards(null, cursorOffset);
                                cursorLine = selection.getLineOfOffset(startOffset);
                            } catch (Exception e) {
                                //may throw error if not balanced or if the char we're at is not a ' or "
                            }

                            if (cursorLine == -1) {
                                cursorLine = selection.getCursorLine();
                            }

                            return new Tuple<String, Boolean>(indentBasedOnStartingScope(text, new PySelection(
                                    selection.getDoc(), cursorLine, 0), false), isInsidePar);

                        }
                    }
                }
            }

        }
        return new Tuple<String, Boolean>(text, false);
    }

    /**
     * @return the text for the indent
     */
    private String indentBasedOnStartingScope(String text, PySelection selection, boolean checkForLowestBeforeNewScope) {
        LineStartingScope previousIfLine = selection.getPreviousLineThatStartsScope();
        if (previousIfLine != null) {
            String initial = getCharsBeforeNewLine(text);

            if (previousIfLine.lineWithDedentWhileLookingScope == null) { //no dedent was found
                String indent = PySelection.getIndentationFromLine(previousIfLine.lineStartingScope);

                if (checkForLowestBeforeNewScope && previousIfLine.lineWithLowestIndent != null) {

                    indent = PySelection.getIndentationFromLine(previousIfLine.lineWithLowestIndent);
                    text = initial + indent;

                } else {

                    text = initial + indent + prefs.getIndentationString();

                }

            } else { //some dedent was found
                String indent = PySelection.getIndentationFromLine(previousIfLine.lineWithDedentWhileLookingScope);
                String indentationString = prefs.getIndentationString();

                final int i = indent.length() - indentationString.length();
                if (i > 0 && indent.length() > i) {
                    text = (initial + indent).substring(0, i + 1);
                } else {
                    text = initial; // this can happen if we found a dedent that is 1 level deep
                }
            }

        }
        return text;
    }

    /**
     * Returns the first offset greater than <code>offset</code> and smaller than
     * <code>end</code> whose character is not a space or tab character. If no such
     * offset is found, <code>end</code> is returned.
     *
     * @param document the document to search in
     * @param offset the offset at which searching start
     * @param end the offset at which searching stops
     * @return the offset in the specified range whose character is not a space or tab
     * @exception BadLocationException if position is an invalid range in the given document
     */
    private int findEndOfWhiteSpace(IDocument document, int offset, int end) throws BadLocationException {
        while (offset < end) {
            char c = document.getChar(offset);
            if (c != ' ' && c != '\t') {
                return offset;
            }
            offset++;
        }
        return end;
    }

    private void autoIndentSameAsPrevious(IDocument d, DocumentCommand c) {
        String txt = autoIndentSameAsPrevious(d, c.offset, c.text, true);
        if (txt != null) {
            c.text = txt;
        }
    }

    /**
     * Copies the indentation of the previous line.
     *
     * @param d the document to work on
     * @param text the string that should added to the start of the returned string
     * @param considerEmptyLines whether we should consider empty lines in this function
     * @param c the command to deal with
     *
     * @return a string with text+ the indentation found in the previous line (or previous non-empty line).
     */
    private String autoIndentSameAsPrevious(IDocument d, int offset, String text, boolean considerEmptyLines) {

        if (offset == -1 || d.getLength() == 0)
            return null;

        try {
            // find start of line
            IRegion info = d.getLineInformationOfOffset(offset);
            String line = d.get(info.getOffset(), info.getLength());

            if (!considerEmptyLines) {
                int currLine = d.getLineOfOffset(offset);
                while (PySelection.containsOnlyWhitespaces(line)) {
                    currLine--;
                    if (currLine < 0) {
                        break;
                    }
                    info = d.getLineInformation(currLine);
                    line = d.get(info.getOffset(), info.getLength());
                }
            }

            int start = info.getOffset();

            // find white spaces
            int end = findEndOfWhiteSpace(d, start, offset);

            FastStringBuffer buf = new FastStringBuffer(text, end - start + 1);
            if (end > start) {
                // append to input
                buf.append(d.get(start, end - start));
            }

            return buf.toString();

        } catch (BadLocationException excp) {
            // stop work
            return null;
        }
    }

    /**
     * @param document
     * @param length
     * @param text
     * @return
     */
    private boolean isNewLineText(IDocument document, int length, String text) {
        return length == 0 && text != null && AbstractIndentPrefs.endsWithNewline(document, text) && text.length() < 3; //could be \r\n
    }

    private String dedent(String text) {
        String indentationString = prefs.getIndentationString();
        int indentationLength = indentationString.length();
        int len = text.length();

        if (len >= indentationLength) {
            text = text.substring(0, len - indentationLength);
        }
        return text;
    }

    private static Tuple<String, Integer> removeFirstIndent(String text, IIndentPrefs prefs) {
        String indentationString = prefs.getIndentationString();
        if (text.startsWith(indentationString)) {
            return new Tuple<String, Integer>(text.substring(indentationString.length()), indentationString.length());
        }
        return new Tuple<String, Integer>(text, 0);
    }

    /**
     * @see org.eclipse.jface.text.IAutoEditStrategy#customizeDocumentCommand(IDocument, DocumentCommand)
     */
    public void customizeDocumentCommand(IDocument document, DocumentCommand command) {
        if (blockSelection) {
            //in block selection, leave all as is and just change tabs/spaces.
            getIndentPrefs().convertToStd(document, command);
            return;
        }
        char c;
        if (command.text.length() == 1) {
            c = command.text.charAt(0);
        } else {
            c = '\0';
        }

        String contentType = ParsingUtils.getContentType(document, command.offset);

        switch (c) {
            case '"':
            case '\'':
                handleLiteral(document, command, contentType.equals(ParsingUtils.PY_DEFAULT), c);
                return;
        }

        // super idents newlines the same amount as the previous line
        final boolean isNewLine = isNewLineText(document, command.length, command.text);

        if (!contentType.equals(ParsingUtils.PY_DEFAULT)) {
            //the indentation is only valid for things in the code (comments should not be indented).
            //(that is, if it is not a new line... in this case, it may have to be indented)
            if (!isNewLine) {
                //we have to take care about tabs anyway
                getIndentPrefs().convertToStd(document, command);
                return;
            } else {
                if (!contentType.equals(ParsingUtils.PY_COMMENT)) {
                    //within string, just regular indent...
                    autoIndentSameAsPrevious(document, command);
                    return;
                }
            }
        }

        try {
            if (isNewLine) {
                customizeNewLine(document, command);
                getIndentPrefs().convertToStd(document, command);
                return;
            }

            if (c == '\0') {
                //In some paste with more contents (c was not set), just convert tabs/spaces and go on...
                getIndentPrefs().convertToStd(document, command);
                return;
            }

            if (c == '\t') {
                handleTab(document, command);
                getIndentPrefs().convertToStd(document, command);
                return;
            }

            getIndentPrefs().convertToStd(document, command);

            switch (c) {
                case '[':
                case '{':
                    if (prefs.getAutoParentesis()) {
                        PySelection ps = new PySelection(document, command.offset);
                        char peer = StringUtils.getPeer(c);
                        if (shouldClose(ps, c, peer)) {
                            command.shiftsCaret = false;
                            command.text = c + "" + peer;
                            command.caretOffset = command.offset + 1;
                        }
                    }
                    return;

                case '(':
                    handleParens(document, command, prefs);
                    return;

                case ':':
                    /*
                     * The following code will auto-replace colons in function
                     * declaractions
                     * e.g.,
                     * def something(self):
                     *                    ^ cursor before the end colon
                     *
                     * Typing another colon (i.e, ':') at that position will not insert
                     * another colon
                     */
                    if (prefs.getAutoColon()) {
                        performColonReplacement(document, command);
                    }

                    /*
                     * Now, let's also check if we are in an 'else:' or 'except:' or 'finally:' that must be dedented in the doc
                     */
                    autoDedentAfterColon(document, command, prefs);
                    return;

                case ' ':
                    /*
                     * this is a space... so, if we are in 'from xxx ', we may auto-write
                     * the import
                     */
                    if (prefs.getAutoWriteImport()) {
                        PySelection ps = new PySelection(document, command.offset);
                        String completeLine = ps.getLineWithoutCommentsOrLiterals();
                        String lineToCursor = ps.getLineContentsToCursor().trim();
                        String lineContentsFromCursor = ps.getLineContentsFromCursor();

                        if (completeLine.indexOf(" import ") == -1
                                && StringUtils.leftTrim(completeLine).startsWith("from ")
                                && !completeLine.startsWith("import ") && !completeLine.endsWith(" import")
                                && !lineToCursor.endsWith(" import") && !lineContentsFromCursor.startsWith("import")) {

                            String importsTipperStr = ImportsSelection.getImportsTipperStr(lineToCursor, false).importsTipperStr;
                            if (importsTipperStr.length() > 0) {
                                command.text = " import ";
                            }
                        }
                    }

                    /*
                     * Now, let's also check if we are in an 'elif ' that must be dedented in the doc
                     */
                    autoDedentElif(document, command, getIndentPrefs());
                    return;

                case ')':
                case ']':
                case '}':
                    /*
                     * If the command is some kind of parentheses or brace, and there's
                     * already a matching one, don't insert it. Just move the cursor to
                     * the next space.
                     */
                    if (prefs.getAutoBraces()) {
                        // you can only do the replacement if the next character already there is what the user is trying to input

                        if (command.offset < document.getLength()
                                && document.get(command.offset, 1).equals(command.text)) {
                            // the following searches through each of the end braces and
                            // sees if the command has one of them

                            boolean found = false;
                            for (int i = 1; i <= StringUtils.BRACKETS.length && !found; i += 2) {
                                char b = StringUtils.BRACKETS[i];
                                if (b == c) {
                                    found = true;
                                    performPairReplacement(document, command);
                                }
                            }
                        }
                    }
                    return;

            }

        }
        /*
         * If something goes wrong, you want to know about it, especially in a
         * unit test. If you don't rethrow the exception, unit tests will pass
         * even though you threw an exception.
         */
        catch (BadLocationException e) {
            // screw up command.text so unit tests can pick it up
            command.text = "BadLocationException";
            throw new RuntimeException(e);
        }
    }

    /**
     * Called right after a '('
     */
    public static void handleParens(IDocument document, DocumentCommand command, IIndentPrefs prefs)
            throws BadLocationException {
        /*
         * Now, let's also check if we are in an 'elif ' that must be dedented in the doc
         */
        autoDedentElif(document, command, prefs);

        customizeParenthesis(document, command, false, prefs);
    }

    /**
     * Called right after a ' or "
     */
    private void handleLiteral(IDocument document, DocumentCommand command, boolean isDefaultContext, char literalChar) {
        if (!prefs.getAutoLiterals()) {
            return;
        }
        PySelection ps = new PySelection(document, new TextSelection(document, command.offset, command.length));
        if (command.length > 0) {
            try {
                //We have more contents selected. Delete it so that we can properly use the heuristics.
                ps.deleteSelection();
                command.length = 0;
                ps.setSelection(command.offset, command.offset);
            } catch (BadLocationException e) {
            }
        }

        try {
            char nextChar = ps.getCharAfterCurrentOffset();
            if (Character.isJavaIdentifierPart(nextChar)) {
                //we're just before a word (don't try to do anything in this case)
                //e.g. |var (| is cursor position)
                return;
            }
        } catch (BadLocationException e) {
        }

        String cursorLineContents = ps.getCursorLineContents();
        if (cursorLineContents.indexOf(literalChar) == -1) {
            if (!isDefaultContext) {
                //only add additional chars if on default context.
                return;
            }
            command.text = StringUtils.getWithClosedPeer(literalChar);
            command.shiftsCaret = false;
            command.caretOffset = command.offset + 1;
            return;
        }

        boolean balanced = isLiteralBalanced(cursorLineContents);

        Tuple<String, String> beforeAndAfterMatchingChars = ps.getBeforeAndAfterMatchingChars(literalChar);

        int matchesBefore = beforeAndAfterMatchingChars.o1.length();
        int matchesAfter = beforeAndAfterMatchingChars.o2.length();

        boolean hasMatchesBefore = matchesBefore != 0;
        boolean hasMatchesAfter = matchesAfter != 0;

        if (!hasMatchesBefore && !hasMatchesAfter) {
            //if it's not balanced, this char would be the closing char.
            if (balanced) {
                if (!isDefaultContext) {
                    //only add additional chars if on default context.
                    return;
                }
                command.text = StringUtils.getWithClosedPeer(literalChar);
                command.shiftsCaret = false;
                command.caretOffset = command.offset + 1;
            }
        } else {
            //we're right after or before a " or '

            if (matchesAfter == 1) {
                //just walk the caret
                command.text = "";
                command.shiftsCaret = false;
                command.caretOffset = command.offset + 1;
            }
        }
    }

    /**
     * @return true if the passed string has balanced ' and "
     */
    private boolean isLiteralBalanced(String cursorLineContents) {
        ParsingUtils parsingUtils = ParsingUtils.create(cursorLineContents, true);

        int offset = 0;
        int end = cursorLineContents.length();
        boolean balanced = true;
        while (offset < end) {
            char curr = cursorLineContents.charAt(offset++);
            if (curr == '"' || curr == '\'') {
                int eaten;
                try {
                    eaten = parsingUtils.eatLiterals(null, offset - 1) + 1;
                } catch (SyntaxErrorException e) {
                    balanced = false;
                    break;
                }
                if (eaten > offset) {
                    offset = eaten;
                }
            }
        }
        return balanced;
    }

    private void handleTab(IDocument document, DocumentCommand command) throws BadLocationException {
        PySelection ps = new PySelection(document, command.offset);
        //it is a tab
        String lineContentsToCursor = ps.getLineContentsToCursor();
        int currSize = lineContentsToCursor.length();
        int cursorLine = ps.getCursorLine();

        //current line is empty
        if (lineContentsToCursor.trim().length() == 0) {
            String nextLine = ps.getLine(cursorLine + 1);

            String prevLine = ps.getLine(cursorLine - 1);
            boolean forceTryOnNext = false;
            if (prevLine.trim().length() == 0) {
                //previous line is empty, so, if the next line has contents, use it to make the match.
                if (nextLine.trim().length() > 0) {
                    forceTryOnNext = true;
                }
            }

            if (forceTryOnNext || nextLine.trim().startsWith("@") || ps.matchesFunctionLine(nextLine)) {
                int firstCharPosition = PySelection.getFirstCharPosition(nextLine);
                if (currSize < firstCharPosition) {
                    String txt = nextLine.substring(currSize, firstCharPosition);
                    //as it's the same indentation from the next line, we don't have to applyDefaultForTab.
                    command.text = txt;
                    return;
                }
            }
        }

        if (cursorLine > 0) {
            //this is to know which would be expected if it was a new line in the previous line
            //(so that we know the 'expected' output
            IRegion prevLineInfo = document.getLineInformation(cursorLine - 1);
            int prevLineEndOffset = prevLineInfo.getOffset() + prevLineInfo.getLength();
            String prevExpectedIndent = autoIndentSameAsPrevious(document, prevLineEndOffset, "\n", false);
            String txt = prevExpectedIndent;
            Tuple<String, Boolean> prevLineTup = autoIndentNewline(document, 0, txt, prevLineEndOffset);
            txt = prevLineTup.o1;
            txt = txt.substring(1);//remove the newline
            prevExpectedIndent = prevExpectedIndent.substring(1);

            if (txt.length() > 0) {
                //now, we should not apply that indent if we are already at the 'max' indent in this line
                //(or better: we should go to that max if it would pass it)
                int sizeExpected = txt.length();
                int sizeApplied = currSize + sizeExpected;

                if (currSize >= sizeExpected) {
                    //ok, we already passed what we expected from the indentation, so, let's indent
                    //to the next 'expected' position...

                    boolean applied = false;
                    //handle within parenthesis
                    if (prevLineTup.o2) {
                        int len = sizeApplied - sizeExpected;
                        if (prevExpectedIndent.length() > len) {
                            command.text = prevExpectedIndent.substring(len);
                            applied = true;
                        }
                    }

                    if (!applied) {
                        applyDefaultForTab(command, currSize);
                    }

                } else if (sizeExpected == sizeApplied) {
                    if (command.length == 0) {
                        ps.deleteSpacesAfter(command.offset);
                    }
                    command.text = txt;
                } else if (sizeApplied > sizeExpected) {
                    ps.deleteSpacesAfter(command.offset);
                    command.text = txt.substring(0, sizeExpected - currSize);
                }
            } else {
                applyDefaultForTab(command, currSize);
            }

        } else { //cursorLine == 0
            applyDefaultForTab(command, currSize);
        }
    }

    public static void customizeParenthesis(IDocument document, DocumentCommand command,
            boolean considerOnlyCurrentLine, IIndentPrefs prefs) throws BadLocationException {
        if (prefs.getAutoParentesis()) {
            PySelection ps = new PySelection(document, command.offset);
            String line = ps.getLine();

            if (shouldClose(ps, '(', ')')) {

                boolean hasClass = line.indexOf("class ") != -1;
                boolean hasClassMethodDef = line.indexOf(" def ") != -1 || line.indexOf("\tdef ") != -1;
                boolean hasMethodDef = line.indexOf("def ") != -1;
                boolean hasDoublePoint = line.indexOf(":") != -1;

                command.shiftsCaret = false;
                if (!hasDoublePoint && (hasClass || hasClassMethodDef || hasMethodDef)) {
                    if (hasClass) {
                        //command.text = "(object):"; //TODO: put some option in the interface for that
                        //command.caretOffset = command.offset + 7;
                        command.text = "():";
                        command.caretOffset = command.offset + 1;

                    } else if (hasClassMethodDef && prefs.getAutoAddSelf()) {
                        String prevLine = ps.getLine(ps.getCursorLine() - 1);
                        if (prevLine.indexOf("@classmethod") != -1) {
                            command.text = "(cls):";
                            command.caretOffset = command.offset + 4;

                        } else if (prevLine.indexOf("@staticmethod") != -1) {
                            command.text = "():";
                            command.caretOffset = command.offset + 1;

                        } else {

                            boolean addRegular = true;
                            if (!considerOnlyCurrentLine) {
                                //ok, also analyze the scope we're in (otherwise, if we only have the current line
                                //that's the best guess we can give).
                                int firstCharPosition = PySelection.getFirstCharPosition(line);

                                LineStartingScope scopeStart = ps.getPreviousLineThatStartsScope(
                                        PySelection.CLASS_AND_FUNC_TOKENS, false, firstCharPosition);

                                if (scopeStart != null) {
                                    if (scopeStart.lineStartingScope != null
                                            && scopeStart.lineStartingScope.indexOf("def ") != -1) {
                                        int iCurrDef = PySelection.getFirstCharPosition(line);
                                        int iPrevDef = PySelection.getFirstCharPosition(scopeStart.lineStartingScope);
                                        if (iCurrDef > iPrevDef) {
                                            addRegular = false;

                                        } else if (iCurrDef == iPrevDef) {
                                            if (scopeStart.lineStartingScope.indexOf("self") == -1) {
                                                //only add self if the one in the same level also has it.
                                                //with a 'gotcha': if it's a classmethod or staticmethod, we
                                                //should still add it.
                                                if (scopeStart.iLineStartingScope <= 0) {
                                                    addRegular = false;
                                                } else {
                                                    addRegular = false;
                                                    int i = scopeStart.iLineStartingScope - 1;
                                                    String line2;
                                                    do {
                                                        line2 = ps.getLine(i).trim();
                                                        i--;
                                                        if (line2.startsWith("@classmethod")
                                                                || line2.startsWith("@staticmethod")) {
                                                            addRegular = true;
                                                            break;
                                                        }
                                                    } while (line2.startsWith("@")); //check all the available decorators...

                                                }
                                            }
                                        }
                                    }
                                } else {
                                    addRegular = false;
                                }
                            }
                            if (addRegular) {
                                command.text = "(self):";
                                command.caretOffset = command.offset + 5;
                            } else {
                                command.text = "():";
                                command.caretOffset = command.offset + 1;
                            }
                        }
                    } else if (hasMethodDef) {
                        command.text = "():";
                        command.caretOffset = command.offset + 1;
                    } else {
                        throw new RuntimeException(PyAutoIndentStrategy.class.toString()
                                + ": customizeDocumentCommand()");
                    }
                } else {
                    command.text = "()";
                    command.caretOffset = command.offset + 1;
                }
            }
        }
    }

    public void customizeNewLine(IDocument document, DocumentCommand command) throws BadLocationException {
        prefs = getIndentPrefs();
        autoIndentSameAsPrevious(document, command);
        if (prefs.getSmartIndentPar()) {
            PySelection selection = new PySelection(document, command.offset);
            if (selection.getCursorLineContents().trim().length() > 0) {
                command.text = autoIndentNewline(document, command.length, command.text, command.offset).o1;
                if (PySelection.containsOnlyWhitespaces(selection.getLineContentsToCursor())) {
                    command.caretOffset = command.offset + selection.countSpacesAfter(command.offset);
                }
            }
        } else {
            PySelection selection = new PySelection(document, command.offset);
            if (selection.getLineContentsToCursor().trim().endsWith(":")) {
                command.text += prefs.getIndentationString();
            }
        }
    }

    /**
     * Updates the text to the next tab position
     * @param command the command to be edited
     * @param lineContentsToCursorLen the current cursor position at the current line
     */
    private void applyDefaultForTab(DocumentCommand command, int lineContentsToCursorLen) {
        IIndentPrefs prefs = getIndentPrefs();
        if (prefs.getUseSpaces(true)) {
            int tabWidth = getIndentPrefs().getTabWidth();

            int mod = (lineContentsToCursorLen + tabWidth) % tabWidth;
            command.text = StringUtils.createSpaceString(tabWidth - mod);
        } else {
            //do nothing (a tab is already a tab)
        }

    }

    /**
     * This function makes the else auto-dedent (if available)
     * @return the new indent and the number of chars it has been dedented (so, that has to be considered as a shift to the left
     * on subsequent things).
     */
    public static Tuple<String, Integer> autoDedentAfterColon(IDocument document, DocumentCommand command, String tok,
            String[] tokens, IIndentPrefs prefs) throws BadLocationException {
        if (prefs.getAutoDedentElse()) {
            PySelection ps = new PySelection(document, command.offset);
            String lineContents = ps.getCursorLineContents();
            if (lineContents.trim().equals(tok)) {

                String previousIfLine = ps.getPreviousLineThatStartsWithToken(tokens);
                if (previousIfLine != null) {
                    String ifIndent = PySelection.getIndentationFromLine(previousIfLine);
                    String lineIndent = PySelection.getIndentationFromLine(lineContents);

                    String indent = prefs.getIndentationString();
                    if (lineIndent.length() == ifIndent.length() + indent.length()) {
                        Tuple<String, Integer> dedented = removeFirstIndent(lineContents, prefs);
                        ps.replaceLineContentsToSelection(dedented.o1);
                        command.offset = command.offset - dedented.o2;
                        return dedented;
                    }
                }
            }
        }
        return null;
    }

    public static Tuple<String, Integer> autoDedentAfterColon(IDocument document, DocumentCommand command,
            IIndentPrefs prefs) throws BadLocationException {
        Tuple<String, Integer> ret = null;
        if ((ret = autoDedentAfterColon(document, command, "else", PySelection.TOKENS_BEFORE_ELSE, prefs)) != null) {
            return ret;
        }
        if ((ret = autoDedentAfterColon(document, command, "except", PySelection.TOKENS_BEFORE_EXCEPT, prefs)) != null) {
            return ret;
        }
        if ((ret = autoDedentAfterColon(document, command, "finally", PySelection.TOKENS_BEFORE_FINALLY, prefs)) != null) {
            return ret;
        }
        return null;
    }

    /**
     * This function makes the else auto-dedent (if available)
     * @return the new indent and the number of chars it has been dedented (so, that has to be considered as a shift to the left
     * on subsequent things).
     */
    public static Tuple<String, Integer> autoDedentElif(IDocument document, DocumentCommand command, IIndentPrefs prefs)
            throws BadLocationException {
        return autoDedentAfterColon(document, command, "elif", PySelection.TOKENS_BEFORE_ELIF, prefs);
    }

    /**
     * Create the indentation string after comma and a newline.
     *
     * @param document
     * @param text
     * @param offset
     * @param selection
     * @return Indentation String
     * @throws BadLocationException
     */
    private String makeSmartIndent(String text, int smartIndent) throws BadLocationException {
        if (smartIndent > 0) {
            String initial = text;

            // Discard everything but the newline from initial, since we'll
            // build the smart indent from scratch anyway.
            initial = getCharsBeforeNewLine(initial);

            // Create the actual indentation string
            String indentationString = prefs.getIndentationString();
            int indentationSteps = smartIndent / prefs.getTabWidth();
            int spaceSteps = smartIndent % prefs.getTabWidth();

            StringBuffer b = new StringBuffer(smartIndent);
            while (indentationSteps > 0) {
                indentationSteps -= 1;
                b.append(indentationString);
            }

            if (prefs.getUseSpaces(true)) {
                while (spaceSteps >= 0) {
                    spaceSteps -= 1;
                    b.append(" ");
                }
            }

            return initial + b.toString();
        }
        return text;
    }

    /**
     * @param initial
     * @return
     */
    private String getCharsBeforeNewLine(String initial) {
        int initialLength = initial.length();
        for (int i = 0; i < initialLength; i++) {
            char theChar = initial.charAt(i);
            // This covers all cases I know of, but if there is any platform
            // with weird newline then this would need to be smarter.
            if (theChar != '\r' && theChar != '\n') {
                if (i > 0) {
                    initial = initial.substring(0, i);
                }
                break;
            }
        }
        return initial;
    }

    /**
     * Private function which is called when a colon is the command.
     *
     * The following code will auto-replace colons in function declaractions
     * e.g., def something(self): ^ cursor before the end colon
     *
     * Typing another colon (i.e, ':') at that position will not insert another
     * colon
     *
     * @param document
     * @param command
     * @throws BadLocationException
     */
    private void performColonReplacement(IDocument document, DocumentCommand command) {
        PySelection ps = new PySelection(document, command.offset);
        int absoluteOffset = ps.getAbsoluteCursorOffset();
        int documentLength = ps.getDoc().getLength();

        // need to check whether whether we're at the very end of the document
        if (absoluteOffset < documentLength) {
            try {
                char currentCharacter = document.getChar(absoluteOffset);

                if (currentCharacter == ':') {
                    command.text = "";
                    command.caretOffset = command.offset + 1;
                }

            } catch (BadLocationException e) {
                // should never happen because I just checked the length
                throw new RuntimeException(e);
            }

        }
    }

    /**
     * Private function to call to perform any replacement of braces.
     *
     * The Eclipse Java editor does this by default, and it is very useful. If
     * you try to insert some kind of pair, be it a parenthesis or bracket in
     * Java, the character will not insert and instead the editor just puts your
     * cursor at the next position.
     *
     * This function performs the equivalent for the Python editor.
     * 
     * @param document
     * @param command if the command does not contain a brace, this function does nothing.
     * @throws BadLocationException
     */
    private void performPairReplacement(IDocument document, DocumentCommand command) throws BadLocationException {
        boolean skipChar = canSkipCloseParenthesis(document, command);
        if (skipChar) {
            //if we have the same number of peers, we want to eat the char
            command.text = "";
            command.caretOffset = command.offset + 1;
        }
    }

    /**
     * @return true if we should skip a ), ] or }
     */
    public boolean canSkipCloseParenthesis(IDocument document, DocumentCommand command) throws BadLocationException {
        PySelection ps = new PySelection(document, command.offset);

        char c = ps.getCharAtCurrentOffset();

        try {
            char peer = StringUtils.getPeer(c);

            FastStringBuffer doc = new FastStringBuffer(document.get(), 2);
            //it is not enough just counting the chars, we have to ignore those that are within comments or literals.
            ParsingUtils.removeCommentsWhitespacesAndLiterals(doc, false);
            int chars = PyAction.countChars(c, doc);
            int peers = PyAction.countChars(peer, doc);

            boolean skipChar = chars == peers;
            return skipChar;
        } catch (NoPeerAvailableException e) {
            return false;
        } catch (SyntaxErrorException e) {
            throw new RuntimeException(e);//not expected!
        }
    }

    /**
     * @return true if we should close the opening pair (parameter c) and false if we shouldn't
     */
    public static boolean shouldClose(PySelection ps, char c, char peer) throws BadLocationException {
        PythonPairMatcher matcher = new PythonPairMatcher(StringUtils.BRACKETS);
        String lineContentsFromCursor = ps.getLineContentsFromCursor();

        for (int i = 0; i < lineContentsFromCursor.length(); i++) {
            char charAt = lineContentsFromCursor.charAt(i);
            if (!Character.isWhitespace(charAt)) {

                if (charAt == ',') {
                    break;
                }
                if (StringUtils.isClosingPeer(charAt)) {
                    break;
                }

                return false;
            }
        }

        //Ok, we have to analyze the current context and see if each closing peer
        //in this context has a match. If one doesn't, we won't close it.
        LineStartingScope nextLineThatStartsScope = ps.getNextLineThatStartsScope();
        int lineStartingNextScope;
        if (nextLineThatStartsScope == null) {
            lineStartingNextScope = Integer.MAX_VALUE;
        } else {
            lineStartingNextScope = nextLineThatStartsScope.iLineStartingScope;
        }

        int closingPeerLine;
        int closingPeerFoundAtOffset = ps.getAbsoluteCursorOffset() - 1; //start to search at the current position

        do {
            //closingPeerFoundAtOffset doesn't need +1 here as it's already added in the matcher.
            closingPeerFoundAtOffset = matcher.searchForClosingPeer(closingPeerFoundAtOffset, c, peer, ps.getDoc());
            if (closingPeerFoundAtOffset == -1) {
                //no more closing peers there, ok to go
                return true;
            }

            //the +1 is needed because we match closing ones that are right before the current cursor
            IRegion match = matcher.match(ps.getDoc(), closingPeerFoundAtOffset + 1);
            if (match == null) {
                //we don't have a match for a close, so, this open is that match.
                return false;
            }

            try {
                closingPeerLine = ps.getDoc().getLineOfOffset(closingPeerFoundAtOffset);
            } catch (Exception e) {
                break;
            }
        } while (lineStartingNextScope > closingPeerLine);

        return true;
    }

    /**
     * Return smart indent amount for new line. This should be done for
     * multiline structures like function parameters, tuples, lists and
     * dictionaries.
     *
     * Example:
     *
     * a=foo(1, #
     *
     * We would return the indentation needed to place the caret at the #
     * position.
     *
     * @param document The document
     * @param offset The document offset of the last character on the previous line
     * @param ps
     * @return indent, or -1 if smart indent could not be determined (fall back to default)
     * and a boolean indicating if we're inside a parenthesis
     */
    public static Tuple<Integer, Boolean> determineSmartIndent(int offset, IDocument document, IIndentPrefs prefs)
            throws BadLocationException {

        PythonPairMatcher matcher = new PythonPairMatcher(StringUtils.BRACKETS);
        int openingPeerOffset = matcher.searchForAnyOpeningPeer(offset, document);
        if (openingPeerOffset == -1) {
            return new Tuple<Integer, Boolean>(-1, false);
        }

        final IRegion lineInformationOfOffset = document.getLineInformationOfOffset(openingPeerOffset);
        //ok, now, if the opening peer is not on the line we're currently, we do not want to make
        //an 'auto-indent', but keep the current indentation level
        boolean openingPeerIsInCurrentLine = PySelection.isInside(offset, lineInformationOfOffset);

        int len = -1;
        String contents = "";
        if (prefs.getIndentToParLevel()) {
            //now, a catch, if we didn't change the indent level, we've to indent in the same level
            //as the previous line, as this means that the user 'customized' the indent level at this place.
            PySelection ps = new PySelection(document, offset);
            String lineContentsToCursor = ps.getLineContentsToCursor();
            if (!openingPeerIsInCurrentLine && !StringUtils.hasUnbalancedClosingPeers(lineContentsToCursor)) {
                try {
                    char openingChar = document.getChar(openingPeerOffset);
                    int closingPeerOffset = matcher.searchForClosingPeer(openingPeerOffset, openingChar,
                            StringUtils.getPeer(openingChar), document);
                    if (closingPeerOffset == -1 || offset <= closingPeerOffset) {
                        return new Tuple<Integer, Boolean>(-1, true); // True because we're inside a parens
                    }

                } catch (Exception e) {
                    Log.log(e);
                    //Something unexpected happened... (document changed?)
                    return new Tuple<Integer, Boolean>(-1, true); // True because we're inside a parens
                }
            }

            //now, there's a little catch here, if we are in a line with an opening peer,
            //we have to choose whether to indent to the opening peer or a little further
            //e.g.: if the line is
            //method(  self <<- a new line here should indent to the start of the self and not
            //to the opening peer.
            if (openingPeerIsInCurrentLine && openingPeerOffset < offset) {
                String fromParToCursor = document.get(openingPeerOffset, offset - openingPeerOffset);
                if (fromParToCursor.length() > 0 && fromParToCursor.charAt(0) == '(') {
                    fromParToCursor = fromParToCursor.substring(1);
                    if (!PySelection.containsOnlyWhitespaces(fromParToCursor)) {
                        final int firstCharPosition = PySelection.getFirstCharPosition(fromParToCursor);
                        openingPeerOffset += firstCharPosition;
                    }
                }
            }

            int openingPeerLineOffset = lineInformationOfOffset.getOffset();
            len = openingPeerOffset - openingPeerLineOffset;
            contents = document.get(openingPeerLineOffset, len);
        } else {
            if (!openingPeerIsInCurrentLine) {
                return new Tuple<Integer, Boolean>(-1, true);
            }

            //ok, don't indent to parenthesis level: Just add the regular indent level
            int line = document.getLineOfOffset(openingPeerOffset);
            final String indent = prefs.getIndentationString();
            contents = PySelection.getLine(document, line);
            contents = PySelection.getIndentationFromLine(contents);
            StringBuffer sb = new StringBuffer();

            //Create the string for the indent level we want.
            for (int i = 0; i < prefs.getIndentAfterParWidth(); i++) {
                sb.append(indent);
            }
            contents += sb.substring(0, sb.length() - 1); //we have to make it -1 (that's what the smartindent expects)
            len = contents.length();
        }
        //add more spaces for each tab
        for (int i = 0; i < contents.length(); i++) {
            if (contents.charAt(i) == '\t') {
                len += prefs.getTabWidth() - 1;
            }
        }
        return new Tuple<Integer, Boolean>(len, true);

    }

    public void setBlockSelection(boolean blockSelection) {
        this.blockSelection = blockSelection;
    }

    public void customizeParenthesis(IDocument doc, DocumentCommand docCmd) throws BadLocationException {
        PyAutoIndentStrategy.customizeParenthesis(doc, docCmd, true, this.getIndentPrefs());
    }

    /**
     * Empty document (should not be written to).
     */
    IDocument EMPTY_DOCUMENT = new Document();

    public String convertTabs(String cmd) {
        DocCmd newStr = new DocCmd(0, 0, cmd);
        getIndentPrefs().convertToStd(EMPTY_DOCUMENT, newStr);
        cmd = newStr.text;
        return cmd;

    }
}
TOP

Related Classes of org.python.pydev.editor.autoedit.PyAutoIndentStrategy

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.